/**
 * @file 	JUnitTestGenerator.java
 * 
 * @date 	2/27/2012
 * 
 * @author 	Aaron Alaniz (aaron.a.alaniz@gmail.com)
 * 
 * @brief	This class implements a junit test generator 
 * 			for input values specified in an Input Value
 * 			Domain.
 *
 */

import java.util.ArrayList;
import java.util.List;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.*;

public class JUnitTestGenerator {

	private static final String testPrefix = "@Test public void test_";
	private static String testMiddle = "() { ";
	
	/**
	 * @brief	Generates a test script that will implement junit tests based on
	 * 			on the values of in the input value domain of a given parameter type.
	 * 
	 * @param 	className
	 * 
	 * @return	junit tests in the form of a string
	 *  
	 * @throws ClassNotFoundException
	 */
	public String createTests(String className) throws ClassNotFoundException {
		ValueDomains domain = new ValueDomains();
		JavaClass jc = Repository.lookupClass(className);
		testMiddle += ((jc.getPackageName() == "") ? ("") : (jc.getPackageName() + ".")) + className + ".";
		ClassGen cg = new ClassGen(jc);
		ConstantPoolGen cpg = cg.getConstantPool();
		String testScript = "";
		Method[] methodsArr = cg.getMethods();
		
		for (int i = 0 ; i < methodsArr.length ; i++) {
			Method m = methodsArr[i];
			String methodName = m.getName();
			MethodGen mg = new MethodGen(m, cg.getClassName(), cpg);
			if (!mg.isStatic()) continue; // ignore instance methods
			Type[] methodParamTypes = mg.getArgumentTypes();
			int numParams = methodParamTypes.length;
			Type curType = null;
			
			switch (numParams) {
			case 0:
				// do nothing
				break;
			case 1:
				curType = methodParamTypes[0];
				List<Object> typeVals = domain.getDomain(curType.toString());
				String curVal = "";
				
				// generate script for each argument in the type's domain
				for (int j = 0 ; j < typeVals.size() ; j++) {
					curVal = typeVals.get(j).toString();
					testScript += generateTest(methodName, j, curVal);
				}
				break;
			default:
				ArrayList<ArrayList<ArrayList<Object>>> testScriptArgs = new ArrayList<ArrayList<ArrayList<Object>>>();
				
				// Initialize parameter super container
				for (int j = 0 ; j < numParams ; j++) {
					testScriptArgs.add(new ArrayList<ArrayList<Object>>());
				}
								
				// Initialize contains with single field vectors
				for (int j = 0 ; j < numParams ; j++) {
					curType = methodParamTypes[j];
					List<Object> curTypeVals = domain.getDomain(curType.toString());
					
					for (int k = 0 ; k < curTypeVals.size() ; k++) {
						testScriptArgs.get(j).add(new ArrayList<Object>());
					}
					
					for(int k = 0 ; k < curTypeVals.size() ; k++) {
						testScriptArgs.get(j).get(k).add(curTypeVals.get(k));
					}
				}
				findAllArguments(testScriptArgs);
				
				for (int j = 0 ; j < testScriptArgs.get(0).size() ; j++) {
					testScript += generateTest(methodName, j, testScriptArgs.get(0).get(j));
				}
				break;
			}
		}
		return testScript;
	}

	/**
	 * @brief	Iterates over the containers and merges the combinations 
	 * 			of inputs in the parameters domains
	 * 
	 * @param 	testScriptArgs
	 */
	private void findAllArguments(ArrayList<ArrayList<ArrayList<Object>>> testScriptArgs) {
		for ( int i = testScriptArgs.size() - 2 ; i >= 0 ; i--) {
			testScriptArgs.add(i, cross(testScriptArgs.get(i), testScriptArgs.get(i + 1)));
		}
	}
	
	/**
	 * @brief	Implements the merging of two value containers in to one.
	 * 
	 * @param 	cont1
	 * 
	 * @param 	cont2
	 * 
	 * @return	Returns a new container with a set of vectors that represent 
	 * 			all the combinations of two containers.
	 */
	private ArrayList<ArrayList<Object>> cross(ArrayList<ArrayList<Object>> cont1, ArrayList<ArrayList<Object>> cont2) {
		ArrayList<ArrayList<Object>> newContainer = new ArrayList<ArrayList<Object>>();
		ArrayList<Object> newVec = null;
		for (int i = 0 ; i < cont1.size() ; i++) {
			for (int j = 0 ; j < cont2.size() ; j++) {
				newVec = mergeContainers(cont1.get(i), cont2.get(j));
				newContainer.add(newVec);
			}
		}
		return newContainer;
	}

	/**
	 * @brief	Implements the merging of two value vectors in to one.
	 * 
	 * @param arr1
	 * 
	 * @param arr2
	 * 
	 * @return Returns a new vector with the merged values.
	 */
	private ArrayList<Object> mergeContainers(ArrayList<Object> arr1, ArrayList<Object> arr2) {
		ArrayList<Object> mergedVec = new ArrayList<Object>();
		
		for(int i = 0 ; i < arr1.size() ; i++) {
			mergedVec.add(arr1.get(i));
		}
		
		for (int i = 0 ; i < arr2.size() ; i++) {
			mergedVec.add(arr2.get(i));
		}
		
		return mergedVec;
	}

	/**
	 * @brief	Generates single test for a single parameter method.
	 * 
	 * @param 	methodName
	 * 
	 * @param 	testNum
	 * 
	 * @param 	arg
	 * 
	 * @return	Returns string representation of test.
	 */
	private String generateTest(String methodName, int testNum, String arg) {
		return testPrefix + methodName + "_" + testNum + testMiddle + methodName + "(" + arg + "); }\n\n";
	}
	
	/**
	 * @brief	Generates a single test for a multi-parameter metehod.
	 * 
	 * @param methodName
	 * 
	 * @param testNum
	 * 
	 * @param args
	 * 
	 * @return	Returns string representation of test.
	 */
	private String generateTest(String methodName, int testNum, ArrayList<Object> args) {
		String generatedTest = testPrefix + methodName + "_" + testNum + testMiddle + methodName + "(";
		Object curVal;
		for (int i = 0 ; i < args.size() ; i++){
			curVal = args.get(i);
			generatedTest += curVal + ((i == args.size() - 1) ? ("); }\n\n") : (", "));
		}
		return generatedTest;
	}

	public static void main(String a[]) throws ClassNotFoundException {
		JUnitTestGenerator testGen = new JUnitTestGenerator();
		System.out.println(testGen.createTests("D"));
	}
}
